Цели:
Задачи:
# загружаю библиотеки
import pandas as pd
import numpy as np
import math as mth
import datetime as dt
import scipy.stats as st
from functools import reduce
import matplotlib.pyplot as plt
%matplotlib inline
import seaborn as sns
from plotly import graph_objects as go
import warnings
warnings.filterwarnings('ignore')
# создаю функцию для просмотра датасета
def first_view(x):
print('-' * 50, '\n', 'Исходный датафрейм:', '\n', '-'*50)
display(x.head())
print('-' * 50, '\n', 'Общая информация о датафрейме:', '\n', '-'*50)
display(x.info())
print('-' * 50, '\n', 'Количество пустых значений в датафрейме:', '\n', '-'*50)
display(x.isna().sum())
print('-' * 50,'\n','Количество явных дубликатов в датафрейме:','\n','-'*50)
display(x.duplicated().sum())
print('-' * 50,'\n','Названия столбцов:','\n','-'*50)
display(x.columns)
# создаю функцию для расчёта статистически значимой разницы
# между долями двух генеральных совокупностей
def stat_value(first_list, second_list, m, alpha):
alpha_holm = []
# получаем номер текущего теста и считаем коррекцию
for i in range(m):
alpha_holm += [alpha / (m - i)]
# основной цикл функции,
# который вычисляет p-value и проверяет нулевую гипотезу
for x in range(0, len(first_list)-1):
successes = np.array([first_list[x+1], second_list[x+1]])
trials = np.array([first_list[x], second_list[x]])
# пропорция успехов в первой группе
p1 = successes[0]/trials[0]
# пропорция успехов во второй группе
p2 = successes[1]/trials[1]
# пропорция успехов в комбинированном датасете:
p_combined = (successes[0] + successes[1]) / (trials[0] + trials[1])
# разница пропорций между группами
difference = p1 - p2
# считаем статистику в ст.отклонениях стандартного нормального распределения
z_value = difference / mth.sqrt(p_combined * (1 - p_combined) * (1/trials[0] + 1/trials[1]))
# задаем стандартное нормальное распределение (среднее 0, ст.отклонение 1)
distr = st.norm(0, 1)
# расчёт значения статистической разницы между группами
p_value = 1 - distr.cdf(z_value)
# вывод полученных результатов
print('Событие:', list(exp_group.index)[x])
print('Значение p-value: {0:.5f}'.format(p_value))
# сравнение полученного p-value с уровнем статистической значимости
test_result = alpha_holm > p_value
print(
'Доля отвергнутых нулевых гипотез {:.1%} для {} тестов с коррекцией Холма\n'
.format(test_result.mean(), len(test_result))
)
Применю функцию с набором методов для просмотра сводной информации.
# загружаю датасет
data = pd.read_csv('logs_exp.csv', sep='\t')
first_view(data)
-------------------------------------------------- Исходный датафрейм: --------------------------------------------------
| EventName | DeviceIDHash | EventTimestamp | ExpId | |
|---|---|---|---|---|
| 0 | MainScreenAppear | 4575588528974610257 | 1564029816 | 246 |
| 1 | MainScreenAppear | 7416695313311560658 | 1564053102 | 246 |
| 2 | PaymentScreenSuccessful | 3518123091307005509 | 1564054127 | 248 |
| 3 | CartScreenAppear | 3518123091307005509 | 1564054127 | 248 |
| 4 | PaymentScreenSuccessful | 6217807653094995999 | 1564055322 | 248 |
-------------------------------------------------- Общая информация о датафрейме: -------------------------------------------------- <class 'pandas.core.frame.DataFrame'> RangeIndex: 244126 entries, 0 to 244125 Data columns (total 4 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 EventName 244126 non-null object 1 DeviceIDHash 244126 non-null int64 2 EventTimestamp 244126 non-null int64 3 ExpId 244126 non-null int64 dtypes: int64(3), object(1) memory usage: 7.5+ MB
None
-------------------------------------------------- Количество пустых значений в датафрейме: --------------------------------------------------
EventName 0 DeviceIDHash 0 EventTimestamp 0 ExpId 0 dtype: int64
-------------------------------------------------- Количество явных дубликатов в датафрейме: --------------------------------------------------
413
-------------------------------------------------- Названия столбцов: --------------------------------------------------
Index(['EventName', 'DeviceIDHash', 'EventTimestamp', 'ExpId'], dtype='object')
Вывод: \ Датасет состооит из 4 столбцов и 244 126 строк. Столбцы содержат следующие данные:
EventName — название события;DeviceIDHash — уникальный идентификатор пользователя;EventTimestamp — время события;ExpId — номер эксперимента: 246 и 247 — контрольные группы, а 248 — экспериментальная.Пропуски не выявлены. Количество явных дубликатов - 413. Названия столбцов требуют преобразования. Столбец EventTimestamp нужно преобразовать к типу данных datetime64[ns]. Данные в столбцах DeviceIDHash и ExpId требуется для удобства дальнейшего исследования преобразовать в object.
data.columns = ['event', 'user_id', 'event_timestamp', 'group']
data.columns
Index(['event', 'user_id', 'event_timestamp', 'group'], dtype='object')
Вывод: \ Для удобства работы с файлом заменил названия столбцов на более ёмкие. Так будет удобнее работать с данными на этапе исследования.
print('Процент дубликатов в рамках всего датафрейма: {:.3%}'
.format(data.duplicated().sum() / data['user_id'].count()))
Процент дубликатов в рамках всего датафрейма: 0.169%
# формирую таблицу с количеством пропусков по столбцам
data[data.duplicated()].groupby('event')['user_id'].count().sort_values(ascending=False).to_frame()
| user_id | |
|---|---|
| event | |
| PaymentScreenSuccessful | 195 |
| MainScreenAppear | 104 |
| CartScreenAppear | 63 |
| Tutorial | 34 |
| OffersScreenAppear | 17 |
data.drop_duplicates(inplace=True)
print('Количество дубликатов после очистки датафрейма:', data.duplicated().sum())
Количество дубликатов после очистки датафрейма: 0
print('Уникальные события:')
list(data['event'].unique())
Уникальные события:
['MainScreenAppear', 'PaymentScreenSuccessful', 'CartScreenAppear', 'OffersScreenAppear', 'Tutorial']
print('Уникальные группы:')
list(data['group'].unique())
Уникальные группы:
[246, 248, 247]
data_dubl_user = reduce(
np.intersect1d, (data[data['group'] == 246]['user_id'],
data[data['group'] == 247]['user_id'],
data[data['group'] == 248]['user_id'])
)
print('Количество повторяющихся пользователей в трех группах:', len(data_dubl_user))
Количество повторяющихся пользователей в трех группах: 0
Вывод: \ Проверил, какой процент явных дубликатов в датасете от общего числа записей и в каком количественном выражении по типу события, а затем все лишние строки удалил. Проверка уникальных значений среди названий событий и групп не выявили неявных дубликатов. Далее посмотрел датасат на повторяющихся пользователей в исследуемых группах, они не были обнаружены.
data[['user_id', 'group']] = data[['user_id', 'group']].astype('str')
data.info()
<class 'pandas.core.frame.DataFrame'> Index: 243713 entries, 0 to 244125 Data columns (total 4 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 event 243713 non-null object 1 user_id 243713 non-null object 2 event_timestamp 243713 non-null int64 3 group 243713 non-null object dtypes: int64(1), object(3) memory usage: 9.3+ MB
Вывод: \
На этапе загрузки датафрейма в 2 столбцах возникла потребность в изменении типа данных с int64 на object. Соответствующую корректировку внес. Столбец event_timestamp содержит данные в виде UNIX timestamp его оставил без изменений, а на следующем этапе добавлю столбцы с нужными форматами дат и времени. Повторно проверил, что корректировка типов данных сработала.
# добавляю в датасет столбцы
data['event_dt'] = pd.to_datetime(data['event_timestamp'], unit='s')
data['dt'] = pd.to_datetime(data['event_timestamp'], unit='s').dt.date
data.head()
| event | user_id | event_timestamp | group | event_dt | dt | |
|---|---|---|---|---|---|---|
| 0 | MainScreenAppear | 4575588528974610257 | 1564029816 | 246 | 2019-07-25 04:43:36 | 2019-07-25 |
| 1 | MainScreenAppear | 7416695313311560658 | 1564053102 | 246 | 2019-07-25 11:11:42 | 2019-07-25 |
| 2 | PaymentScreenSuccessful | 3518123091307005509 | 1564054127 | 248 | 2019-07-25 11:28:47 | 2019-07-25 |
| 3 | CartScreenAppear | 3518123091307005509 | 1564054127 | 248 | 2019-07-25 11:28:47 | 2019-07-25 |
| 4 | PaymentScreenSuccessful | 6217807653094995999 | 1564055322 | 248 | 2019-07-25 11:48:42 | 2019-07-25 |
Вывод: \ В датафрейм добавил столбцы с датой и временем и просто с датой.
print('Общее количество событий в логе: {}\n'
.format(data['event'].count())
)
print('Общее количество пользоватлей в логе: {}\n'
.format(data['user_id'].nunique())
)
print('Среднее количество событий на пользователя: {:.2f}\n'
.format((data['event'].count() / data['user_id'].nunique()))
)
print('Минимальная дата события:', data['event_dt'].min())
print('Максимальная дата события:', data['event_dt'].max())
Общее количество событий в логе: 243713 Общее количество пользоватлей в логе: 7551 Среднее количество событий на пользователя: 32.28 Минимальная дата события: 2019-07-25 04:43:36 Максимальная дата события: 2019-08-07 21:15:17
# строю визуализацию
plt.figure(figsize=(15, 6))
sns.histplot(data=data, x='dt', hue='group', multiple='dodge', palette='Set1', shrink=.9,)
plt.grid(axis='y')
plt.xticks(rotation=45)
# обозначаю границу отсечения
plt.axvline(x=dt.datetime(2019, 7, 31, 12), color='grey', linestyle='--')
# указываю названия графика и осей
plt.title('Количество событий в зависимости от времени в разрезе групп', size=14)
plt.xlabel('Дата')
plt.ylabel('Количество событий')
plt.show()
print('Количество событий из прошлого: {}'
.format(len(data.query('event_dt < "2019-08-01"'))))
print('Процент событий из прошлого: {:.2%}'
.format(len(data.query('event_dt < "2019-08-01"')) / len(data)))
Количество событий из прошлого: 2826 Процент событий из прошлого: 1.16%
print('Количество пользователей из прошлого: {}'
.format(data['user_id'].nunique() -\
data.query('event_dt >= "2019-08-01"')['user_id'].nunique()))
print('Процент пользователей из прошлого: {:.2%}'
.format((data['user_id'].nunique() -\
data.query('event_dt >= "2019-08-01"')['user_id'].nunique()) /\
data['user_id'].nunique()))
Количество пользователей из прошлого: 17 Процент пользователей из прошлого: 0.23%
# подготавливаю датасет для дальнейшего исследования
data_research = data.query('event_dt >= "2019-08-01"')
Вывод: \
Построил гистограмму, по которой очень четко видна разница в наполнении каждого дня по каждому событию. Все данные до 2019-08-01 брать для дальнейшего исследования не верно, так как это может «перекашивать данные». Именно с этой даты данные полные и логично отбросить более старые. Дополнительно проверил процент "отброшенных" событий - 2 826, что менее 1.5%, а количество пользователей - 17 или 0.23%. Такие потери допустимы.
# формирую таблицу с данными по группам
data_research.groupby('group').agg(count_events = ('event', 'count'),
users = ('user_id', 'nunique'))
| count_events | users | |
|---|---|---|
| group | ||
| 246 | 79302 | 2484 |
| 247 | 77022 | 2513 |
| 248 | 84563 | 2537 |
Вывод: \ Все три группы состоят из почти равного числа пользователей и приближенного по количеству числа событий.
# создаю сводную таблицу
users_count = (
data_research.groupby('event')
.agg(count_events = ('event', 'count'),
users = ('user_id', 'nunique'))
)
users_count['users_part'] = (
users_count['users'] / data_research['user_id']
.nunique() * 100).round(2)
users_count.sort_values(
by='count_events',
ascending=False,
inplace=True)
users_count
| count_events | users | users_part | |
|---|---|---|---|
| event | |||
| MainScreenAppear | 117328 | 7419 | 98.47 |
| OffersScreenAppear | 46333 | 4593 | 60.96 |
| CartScreenAppear | 42303 | 3734 | 49.56 |
| PaymentScreenSuccessful | 33918 | 3539 | 46.97 |
| Tutorial | 1005 | 840 | 11.15 |
Вывод: \
Провел расчет количества и доли пользователей по каждому из событий и повторяемость самих событий. В логах представлены 5 типов событий, самое частотное - MainScreenAppear в 2.5 раза превосходит следующее, по количеству пользователей в 1.6 раза. Самое редко встречающееся - Tutorial, это вполне закономерно, так как инструкции по работе с приложениями обычно не читают, юзеры ориентируются по факту пользования интерфейсом. Стоит заметить что доля пользователей в собитии MainScreenAppear составляет не 100%, что может говорить о переходе по рекламному баннеру или по ссылке через поисковый запрос.\
Можно предположить, что события происходят в следующем порядке:
MainScreenAppear - главный экран;OffersScreenAppear - экран карточки товара;CartScreenAppear - корзина;PaymentScreenSuccessful - страница успешной оплаты;Tutorial - инструкция по работе с приложением. Данное событие не возможно точно встроить в цепочку действий. Правильно будет не учитывать его при расчёте воронки.# добавляю столбце с коэффициентом оттока
users_count['churn_rate'] = (
users_count['users'] / users_count['users']
.shift(1, fill_value = users_count['users'].max()) * 100).round(2)
# исключаю данные с Tutorial
users_count.query('index != "Tutorial"', inplace=True)
users_count
| count_events | users | users_part | churn_rate | |
|---|---|---|---|---|
| event | ||||
| MainScreenAppear | 117328 | 7419 | 98.47 | 100.00 |
| OffersScreenAppear | 46333 | 4593 | 60.96 | 61.91 |
| CartScreenAppear | 42303 | 3734 | 49.56 | 81.30 |
| PaymentScreenSuccessful | 33918 | 3539 | 46.97 | 94.78 |
print('Доля пользователей доходящая от первого события до оплаты: {:.2%}'
.format(users_count.loc['PaymentScreenSuccessful', 'users'] / users_count['users'].max()))
Доля пользователей доходящая от первого события до оплаты: 47.70%
# строю визуализацию
fig = go.Figure()
fig.add_trace(go.Funnel(
name = 'users',
orientation = 'h',
y = users_count.index,
x = users_count['users'],
textinfo = 'value+percent initial'))
fig.update_layout(title='Воронка событий', title_x=0.5)
fig.show()
Вывод: \ По факту расчета видно сильное западение при переходе на второй этап - около 40%. Оставшиеся 2 шага показывают обратный результат: более 80% юзеров, открывших карточку товара, положили товар в карзину, и практически все из них (95%) совершили оплату за заказ, что перевело их в разряд покупателей. \ Общая конверсия составила 47.7%, что является отличным показателем.\ Визуализировал воронку для наглядного понимания оттока пользователей между событиями.
# создаю сводную таблицу с количеством уникальных пользователей
total_users = data_research.pivot_table(columns='group', values='user_id', aggfunc='nunique')
total_users
| group | 246 | 247 | 248 |
|---|---|---|---|
| user_id | 2484 | 2513 | 2537 |
# подготавливаю датасет для визуализации
exp_group = (
data_research
.query('event != "Tutorial"')
.pivot_table(index='event',
columns='group',
values='user_id',
aggfunc='nunique')
)
exp_group.sort_values(by='246',
ascending=False,
inplace=True)
# строю визуализации
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(15,7))
palette = sns.color_palette('Set2')[0:4]
fig.suptitle('Количество пользователей по событиям в контрольных группах', size=18)
ax1.pie(x=exp_group['246'], labels = exp_group.index,
autopct='%1.1f%%', colors = palette, normalize=True,
wedgeprops={'lw':1, 'ls':'-','edgecolor':"k"},
explode = (0.1, 0, 0, 0))
ax1.set_title('Группа 246', size=14)
ax2.pie(x=exp_group['247'], labels = exp_group.index,
autopct='%1.1f%%', colors = palette, normalize=True,
wedgeprops={'lw':1, 'ls':'-','edgecolor':"k"},
explode = (0.1, 0, 0, 0))
ax2.set_title('Группа 247', size=14)
fig.tight_layout()
exp_group.plot(kind='pie', y='248', labels=exp_group.index,
autopct='%1.1f%%', colors = palette, normalize=True,
wedgeprops={'lw':1, 'ls':'-','edgecolor':"k"},
explode = (0.1, 0, 0, 0), legend=False, figsize=(7, 7))
plt.ylabel('')
plt.title('Количество пользователей по событиям в экспериментальной группе - 248', size=18)
exp_group.T
| event | MainScreenAppear | OffersScreenAppear | CartScreenAppear | PaymentScreenSuccessful |
|---|---|---|---|---|
| group | ||||
| 246 | 2450 | 1542 | 1266 | 1200 |
| 247 | 2476 | 1520 | 1238 | 1158 |
| 248 | 2493 | 1531 | 1230 | 1181 |
Сделал расчет количества пользователей по контрольным (246 и 247) и экспериментальной (248) группам и в разбивке по событиям. Для наглядности визуализировал. По графикам видно, что по соотношению долей группы очень близки, но особо отмечается сходство группы 247 и 248: в большенстве событий идентичны.
Для удобства работы объединю показания таблицы с количеством уникальным пользователей и таблицы с разбивкой пользователей по событиям. Напишу функцию для расчёта статистически значимой разницы между долями двух генеральных совокупностей, так как она будет использоваться для каждого события последовательно.
# объединяю таблицы
total_users.reset_index(inplace=True)
total_users.columns = ['event', '246', '247', '248']
exp_users = pd.concat([total_users, exp_group.reset_index()], ignore_index=True)
exp_users
| event | 246 | 247 | 248 | |
|---|---|---|---|---|
| 0 | user_id | 2484 | 2513 | 2537 |
| 1 | MainScreenAppear | 2450 | 2476 | 2493 |
| 2 | OffersScreenAppear | 1542 | 1520 | 1531 |
| 3 | CartScreenAppear | 1266 | 1238 | 1230 |
| 4 | PaymentScreenSuccessful | 1200 | 1158 | 1181 |
Гипотезы: \ Нулевая гипотеза (H0): статистически значимые различия между долями двух генеральных совокупностей нет. \ Альтернативная гипотеза (H1): статистически значимые различия между долями двух генеральных совокупностей есть.
Параметры для проверки всех последующих гипотез: \ Статистическая значимость: (alpha) 0.01. Так как проводится A/A-тестирование, уровень значимости устанавлю на минимально возможном значении. \ Метод для проверки гипотез: Проверка гипотезы о равенстве долей - так как если некоторая доля генеральной совокупности обладает признаком, а другая её часть — нет, об этой доле можно судить по выборке из генеральной совокупности. Как и в случае со средним, выборочные доли будут нормально распределены вокруг настоящей. \ Поправка на множественную проверку гипотез: Метод Холма - он в среднем даёт более высокие значения уровня значимости, поскольку он более «щадящий» по отношению к мощности теста.
# применяю функцию для расчёта статистически значимой разницы в контрольных группах
stat_value(list(exp_users['246']), list(exp_users['247']), 16, 0.01)
Событие: MainScreenAppear Значение p-value: 0.37853 Доля отвергнутых нулевых гипотез 0.0% для 16 тестов с коррекцией Холма Событие: OffersScreenAppear Значение p-value: 0.13112 Доля отвергнутых нулевых гипотез 0.0% для 16 тестов с коррекцией Холма Событие: CartScreenAppear Значение p-value: 0.31969 Доля отвергнутых нулевых гипотез 0.0% для 16 тестов с коррекцией Холма Событие: PaymentScreenSuccessful Значение p-value: 0.09122 Доля отвергнутых нулевых гипотез 0.0% для 16 тестов с коррекцией Холма
Вывод: \ Гипотеза о равенстве пропорций двух контрольных групп для А/А-эксперимента не отверглась, статистическая разница между выборками 246 и 247 отсутствует во всех событиях и при всех уровнях статистической значимости с учетом корректировки Холма на множественную проверку гипотез.
Поскольку две контрольные группы подходят для продолжения эксперимента, то целесообразно будет провести тестирование каждой контрольной группы с экспериментальной по отдельности, затем сравнить совокупное значение контрольных групп (246 и 247) с экспериментальной (248).
# добавляю в датасет столбец с совокупными данными контрольных групп
exp_users['246_247'] = exp_users['246'] + exp_users['247']
exp_users
| event | 246 | 247 | 248 | 246_247 | |
|---|---|---|---|---|---|
| 0 | user_id | 2484 | 2513 | 2537 | 4997 |
| 1 | MainScreenAppear | 2450 | 2476 | 2493 | 4926 |
| 2 | OffersScreenAppear | 1542 | 1520 | 1531 | 3062 |
| 3 | CartScreenAppear | 1266 | 1238 | 1230 | 2504 |
| 4 | PaymentScreenSuccessful | 1200 | 1158 | 1181 | 2358 |
Для трех последующих тестов будет применены следующие гипотезы и параментры.
Гипотезы: \ Нулевая гипотеза (H0): статистически значимые различия между долями двух генеральных совокупностей нет. \ Альтернативная гипотеза (H1): статистически значимые различия между долями двух генеральных совокупностей есть.
Параметры для проверки всех последующих гипотез: \ Статистическая значимость: (alpha) 0.05. \ Метод для проверки гипотез: Проверка гипотезы о равенстве долей. \ Поправка на множественную проверку гипотез: Метод Холма.
# применяю функцию для расчёта статистически значимой разницы
# в контрольной и экспериментальной группах
stat_value(list(exp_users['246']), list(exp_users['248']), 16, 0.05)
Событие: MainScreenAppear Значение p-value: 0.14749 Доля отвергнутых нулевых гипотез 0.0% для 16 тестов с коррекцией Холма Событие: OffersScreenAppear Значение p-value: 0.13421 Доля отвергнутых нулевых гипотез 0.0% для 16 тестов с коррекцией Холма Событие: CartScreenAppear Значение p-value: 0.10561 Доля отвергнутых нулевых гипотез 0.0% для 16 тестов с коррекцией Холма Событие: PaymentScreenSuccessful Значение p-value: 0.92852 Доля отвергнутых нулевых гипотез 0.0% для 16 тестов с коррекцией Холма
Гипотеза о равенстве пропорций двух групп для А/В-эксперимента не отверглась, статистическая разница между выборками 246 и 248 отсутствует во всех событиях и при всех уровнях статистической значимости с учетом корректировки Холма на множественную проверку гипотез.
# применяю функцию для расчёта статистически значимой разницы
# в контрольной и экспериментальной группах
stat_value(list(exp_users['247']), list(exp_users['248']), 16, 0.05)
Событие: MainScreenAppear Значение p-value: 0.22935 Доля отвергнутых нулевых гипотез 0.0% для 16 тестов с коррекцией Холма Событие: OffersScreenAppear Значение p-value: 0.50653 Доля отвергнутых нулевых гипотез 0.0% для 16 тестов с коррекцией Холма Событие: CartScreenAppear Значение p-value: 0.21825 Доля отвергнутых нулевых гипотез 0.0% для 16 тестов с коррекцией Холма Событие: PaymentScreenSuccessful Значение p-value: 0.99716 Доля отвергнутых нулевых гипотез 0.0% для 16 тестов с коррекцией Холма
Гипотеза о равенстве пропорций двух групп для А/В-эксперимента не отверглась, статистическая разница между выборками 247 и 248 отсутствует во всех событиях и при всех уровнях статистической значимости с учетом корректировки Холма на множественную проверку гипотез.
# применяю функцию для расчёта статистически значимой разницы
# в совокупности контрольных и экспериментальной группах
stat_value(list(exp_users['246_247']), list(exp_users['248']), 16, 0.05)
Событие: MainScreenAppear Значение p-value: 0.14712 Доля отвергнутых нулевых гипотез 0.0% для 16 тестов с коррекцией Холма Событие: OffersScreenAppear Значение p-value: 0.26543 Доля отвергнутых нулевых гипотез 0.0% для 16 тестов с коррекцией Холма Событие: CartScreenAppear Значение p-value: 0.11953 Доля отвергнутых нулевых гипотез 0.0% для 16 тестов с коррекцией Холма Событие: PaymentScreenSuccessful Значение p-value: 0.99144 Доля отвергнутых нулевых гипотез 0.0% для 16 тестов с коррекцией Холма
Гипотеза о равенстве пропорций двух контрольных групп и экспериментальной для А/A/В-эксперимента не отверглась, статистическая разница между выборками 246-247 и 248 отсутствует во всех событиях и при всех уровнях статистической значимости с учетом корректировки Холма на множественную проверку гипотез.
Вывод: \ Так как была проведена множествыенная проверка гипотех, то для чистоты эксперимента применил корректировку Холма для уровня статистической значимости. В ходе проведения А/B и A/A/B-тестов, удалось выяснить, что между количеством пользователей, совершивших каждое событие, в контрольных и экспериментальной группах нет статистически значимой разницы, можно считать, что доля пользователей ,совершивших одно и тоже событие, одинакова для всех групп и экспериментах. \ Следовательно, можно сделать вывод, что новые шрифты, которые показывали пользователям экспериментальной группы (248) никак не повлияли на поведение пользователей внутри приложения.
Чтобы справиться с проблемой множественного сравнения гипотез, воспользовался дополнительно калькулятором Chi-Squared Test. По всем группам не выявлена статистически значимые различия.
Для проведения тестирования был предоставлен датасет:
2019-07-25 по 2019-08-07;Проведены работы по изучению и проверке данных:
2019-08-01 по 2019-08-07;Изучена воронка событий и её последовательность:
MainScreenAppear - главный экран, данное событие было вызвано 117 328 раз;Tutorial - инструкция по работе с приложением, данное событие было вызвано всего 1005 раз и для формирования воронки данные этого события не использовались;OffersScreenAppear, данное событие совершает только 62% пользователей от общего количества пользователей, совершивших предыдущее событие. На остальных шагах воронки потери пользователей не такие серьёзные.Результаты тестирований:
Рекомендации: